#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/uio.h>

#define LIO_NODE_MAX 3

#define MINOR_BASE	  0
#define DRIVER_NAME	 "lio"
#define DEVICE_NAME_FREFIX "lio"
#define CLASS_NAME	  "lio"

struct property_s {
	char name[32];
	int shift;
	int bitwidth;
};

static struct property_s g_lio_prop[] =
#if defined(CONFIG_LOONGSON_2K500)
{
	{"lio-rom-width", 28, 1},
	{"lio-rom-count-init", 23, 5},
	{"lio-clk-period", 21, 2},
};
#elif defined(CONFIG_LOONGSON_2K1000)
{
	{"lio-big-mem", 63, 1},
	{"lio-io-width", 61, 1},
	{"lio-io-count-init", 56, 5},
	{"lio-rom-width", 55, 1},
	{"lio-rom-count-init", 50, 5},
	{"lio-clk-period", 48, 2},
};
#else
{
};
#endif

/**
 * lio_dev
 * 	|-------+-------.
 * 	V	V       V
 * 	node0   node1   node2
 * 	0x1C10  0x1FD0  0x1FF0
 * 	  0000    0000    0000
 */

struct lio_dev_s {
	const char* name;	// name from pdev
	u8 lio_big_mem;
	u8 lio_io_width;	// 0:8bit, 1:16bit
	u8 lio_io_count_init;
	u8 lio_rom_width;	// 0:8bit, 1:16bit
	u8 lio_rom_count_init;
	u8 lio_clk_period;

	struct class *class;
	int node_num;
	struct list_head lio_nodes;
};

struct lio_node {
	struct resource* resource;
	void __iomem *base;
	u32 size;

	dev_t dev_num;
	struct cdev cdev;
	struct device *dev;
	int index;
	struct list_head list;

	struct lio_dev_s* parent;
};

static struct lio_dev_s g_lio_dev = {0};

static int lio_select_node(struct inode *inode, struct file* file)
{
	struct lio_node* node;
	struct lio_dev_s* lio_dev = &g_lio_dev;

	list_for_each_entry(node, &lio_dev->lio_nodes, list) {
		if (node->dev_num == inode->i_rdev)
		{
//			printk("open dev %d:%d\n",
//					MAJOR(inode->i_rdev), MINOR(inode->i_rdev));
			file->private_data = node;
			return 0;
		}
	}

	printk("[%s] find lio node fail\n", __func__);
	return -1;
}

static int lio_open (struct inode *inode, struct file *file)
{
	//printk("%s:%d\n", __FUNCTION__, __LINE__);
	return lio_select_node(inode, file);
}

static loff_t lio_llseek (struct file *file, loff_t offset, int whence)
{
	loff_t ret = -EINVAL;
	struct lio_node* node = (struct lio_node*)(file->private_data);

	//printk("%s:%d offset:0x%llx whence:%d\n", __FUNCTION__, __LINE__, offset, whence);
	if (offset  >= node->size) {
		pr_info("The max size is: 0x%x, offset:0x%llx\n",
							node->size, offset);
		return EFAULT;
	}

	switch(whence) {
		case SEEK_END:
		case SEEK_DATA:
		case SEEK_HOLE:
			/* unsupported */
			return -EINVAL;
		case SEEK_CUR:
			if (offset == 0)
				return file->f_pos;

			offset += file->f_pos;
			break;
		case SEEK_SET:
			break;
	}

	if (offset != file->f_pos) {
		file->f_pos = offset;
		file->f_version = 0;
		ret = offset;
	}

	return ret;
}

static ssize_t lio_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	unsigned long res;
	u8 *kbuf;
	u16 val16;
	void *ptr;
	int i;
	struct lio_node* node = (struct lio_node*)(file->private_data);

	//printk("%s:%d size:%ld, offset:0x%llx\n", __FUNCTION__, __LINE__, size, *offset);

	if ((*offset + size) >= node->size) {
		pr_info("The max size is: 0x%x, size:0x%lx, offset:0x%llx\n",
				node->size, size, *offset);
		return EFAULT;
	}

	kbuf = kmalloc(size+1, GFP_KERNEL);
	if (!kbuf) {
		return -ENOMEM;
	}

	ptr = node->base + *offset;
	if (node->parent->lio_io_width == 0) {
		for (i = 0; i < size; i++) {
			kbuf[i] = ioread8(ptr + i);
		}
	} else {
		for (i = 0; i < size; i += 2) {
			val16 = ioread16(ptr + i);
			kbuf[i+1] = val16 >> 8;
			kbuf[i] = val16 & 0xff;
		}
	}

	// pr_info("lio kernel read: 0x%x\n", kbuf[0]);
	res = copy_to_user(buf, kbuf, size);
	if (res != 0) {
		printk("lio copy to user fail.\n");
		goto fail;
	}

	return size;
fail:
	kfree(kbuf);
	return -EFAULT;
}

static ssize_t lio_write (struct file *file,
		const char __user *buf, size_t size, loff_t *offset)
{
	unsigned long res;
	u8 *kbuf;
	void *ptr;
	int i;
	struct lio_node* node = (struct lio_node*)(file->private_data);

	//printk("%s:%d.\n", __FUNCTION__, __LINE__);
	if ((*offset + size) >= node->size) {
		pr_info("The max size is: 0x%x, size:0x%lx, offset:0x%llx\n",
				node->size, size, *offset);
		return EFAULT;
	}

	kbuf = kmalloc(size+1, GFP_KERNEL);
	if (!kbuf) {
		return -ENOMEM;
	}

	res = copy_from_user(kbuf, buf, size);
	if (res != 0) {
		printk("lio copy fail.\n");
		goto fail;
	}

	ptr = node->base + *offset;
	if (node->parent->lio_io_width == 0) {
		for (i = 0; i < size; i++) {
			iowrite8(kbuf[i], ptr+i);
		}
	} else {
		for (i = 0; i < size; i += 2) {
			iowrite16((kbuf[i+1] << 8 | kbuf[i]), ptr+i);
		}
	}

	return size;
fail:
	kfree(kbuf);
	return -EFAULT;
}

static int lio_release (struct inode *indoe, struct file *file)
{
	//printk("%s:%d\n", __FUNCTION__, __LINE__);
	return 0;
}

static struct file_operations fops = {
	.owner = THIS_MODULE,
	.open = lio_open,
	.read = lio_read,
	.write = lio_write,
	.llseek = lio_llseek,
	.release = lio_release
};

static int lio_sync_config_to_reg(struct device_node *of_node,
		struct lio_dev_s* lio_dev)
{
#if defined(CONFIG_LOONGSON_2K500)
	#define CHIP_CONF			0x1fe10104
	u32 val = readl((void __iomem *)TO_UNCAC(CHIP_CONF));
	u32 mask;
	u32 tmp;
#elif defined(CONFIG_LOONGSON_2K1000)
	#define CHIP_CONF			0x1fe00420
	u64 val = readq((void __iomem *)TO_UNCAC(CHIP_CONF));
	u64 mask;
	u64 tmp;
#else
	u32 val = 0;
	u32 mask;
	u32 tmp;
#endif
	int i;
	u32 prop;
	int shift;
	int bitwidth;

	for (i = 0; i < ARRAY_SIZE(g_lio_prop); i++)
	{
		if (of_property_read_u32(of_node, g_lio_prop[i].name, &prop) == 0)
		{
			tmp = prop; // u32 << 63~32 will get 0
			shift = g_lio_prop[i].shift;
			bitwidth = g_lio_prop[i].bitwidth;
			mask = GENMASK(shift + bitwidth - 1, shift);
			val = (val & (~mask)) | ((tmp << shift) & mask);
		}
	}

#if defined(CONFIG_LOONGSON_2K500)
	writel(val, (void __iomem *)TO_UNCAC(CHIP_CONF));
#elif defined(CONFIG_LOONGSON_2K1000)
	writeq(val, (void __iomem *)TO_UNCAC(CHIP_CONF));
#else
#endif
	return 0;
}

static int lio_record_config(struct device_node *of_node,
		struct lio_dev_s* lio_dev)
{
	int ret;
	u32 val;

	ret = of_property_read_u32(of_node, "lio-big-mem", &val);
	if (ret < 0) {
		pr_warn("lio lio-big-mem not find.\n");
		val = 0;
	}
	lio_dev->lio_big_mem = val;

	ret = of_property_read_u32(of_node, "lio-io-width", &val);
	if (ret < 0) {
		pr_warn("lio lio-io-width not find.\n");
		val = 0;
	}
	lio_dev->lio_io_width = val;

	ret = of_property_read_u32(of_node, "lio-io-count-init", &val);
	if (ret < 0) {
		pr_warn("lio lio-io-count-init not find.\n");
		val = 0;
	}
	lio_dev->lio_io_count_init = val;

	ret = of_property_read_u32(of_node, "lio-rom-width", &val);
	if (ret < 0) {
		pr_warn("lio lio-rom-width not find.\n");
		val = 0;
	}
	lio_dev->lio_rom_width = val;

	ret = of_property_read_u32(of_node, "lio-rom-count-init", &val);
	if (ret < 0) {
		pr_warn("lio-rom-count-init not find.\n");
		val = 0;
	}
	lio_dev->lio_rom_count_init = val;

	ret = of_property_read_u32(of_node, "lio-clk-period", &val);
	if (ret < 0) {
		pr_warn("lio-clk-period not find.\n");
		val = 0;
	}
	lio_dev->lio_clk_period = val;

	return 0;
}

static int lio_release_resource(struct lio_dev_s* lio_dev)
{
	struct lio_node* node;
	list_for_each_entry(node, &lio_dev->lio_nodes, list) {
		if (node->base != NULL)
			iounmap(node->base);

		if (node->resource != NULL)
			release_mem_region(node->resource->start,
					resource_size(node->resource));

		kfree(node);
	}

	list_del(&lio_dev->lio_nodes);

	lio_dev = 0;

	return 0;
}

static int ls2k1000_lionode_append(struct lio_dev_s* lio_dev, struct resource * mem)
{
	struct lio_node* node = kzalloc(sizeof(*node), GFP_KERNEL);
	if (node == NULL)
		goto FAIL;

	node->resource = request_mem_region(mem->start,
			resource_size(mem), lio_dev->name);
	if (node->resource == NULL)
		goto KFREE_NODE;

	node->base = ioremap(node->resource->start,
			resource_size(node->resource));
	if (node->base == NULL)
		goto RELEASE_REGION;

	node->size = resource_size(mem);
	node->index = lio_dev->node_num;
	node->parent = lio_dev;

	list_add_tail(&node->list, &lio_dev->lio_nodes);
	lio_dev->node_num++;

	printk("[%s] mem_region <0x%llx, 0x%llx> OK\n", __func__,
			mem->start, resource_size(mem));
	return 0;
RELEASE_REGION:
	printk("[%s] ioremap <0x%llx, 0x%llx> fail\n", __func__,
			node->resource->start, resource_size(node->resource));
	release_mem_region(node->resource->start,
			resource_size(node->resource));
KFREE_NODE:
	printk("[%s] mem_region <0x%llx, 0x%llx> fail\n", __func__,
			mem->start, resource_size(mem));
	kfree(node);
FAIL:
	printk("[%s] kzalloc fail\n", __func__);
	return -1;
}

static int lio_aquire_resource(struct platform_device* pdev,
		struct lio_dev_s* lio_dev)
{
	int ret;
	struct resource *mem;
	int i;

	INIT_LIST_HEAD(&lio_dev->lio_nodes);
	lio_dev->name = pdev->name;

	for (i = 0; i < LIO_NODE_MAX; i++)
	{
		mem = platform_get_resource(pdev, IORESOURCE_MEM, i);
		if (mem == NULL)
			goto CHECK_NUM;

		ret = ls2k1000_lionode_append(lio_dev, mem);
		if (ret != 0)
			goto FAIL;
	}

	return 0;

CHECK_NUM:
	printk("[%s] resource empty\n", __func__);
	if (lio_dev->node_num == 0)
		return -1;
	else
		return 0;

FAIL:
	lio_release_resource(lio_dev);
	return -1;

}

static int lio_free_device_region(struct lio_dev_s* lio_dev)
{
	struct lio_node* node;
	list_for_each_entry(node, &lio_dev->lio_nodes, list) {
//		if (DEV_T_VALID(node->dev_num))
			unregister_chrdev_region(node->dev_num, 1);
	}
	return 0;
}

static int lio_alloc_device_region(struct lio_dev_s* lio_dev)
{
	int ret = 0;
	struct lio_node* node;
	list_for_each_entry(node, &lio_dev->lio_nodes, list) {
		ret = alloc_chrdev_region(&node->dev_num,
				MINOR_BASE, 1, DRIVER_NAME);
		if (ret != 0)
		{
			printk("[%s] chrdev fail <%d = %d:%d>\n", __func__,
					ret, MAJOR(node->dev_num), MINOR(node->dev_num));
			goto FAIL;
		}
		printk("alloc chrdev region success.%d:%d\n",
				MAJOR(node->dev_num), MINOR(node->dev_num));
	}

	return 0;
FAIL:
	lio_free_device_region(lio_dev);
	return ret;
}

static int lio_remove_device_node(struct lio_dev_s* lio_dev)
{
	struct lio_node* node;

	if (lio_dev->class == NULL)
		return 0;

	list_for_each_entry(node, &lio_dev->lio_nodes, list) {
		if (node->dev != NULL)
			device_destroy(lio_dev->class, node->dev_num);

		cdev_del(&node->cdev);
	}

	class_destroy(lio_dev->class);
	lio_dev->class = NULL;

	return 0;
}

static int lio_create_device_node(struct lio_dev_s* lio_dev)
{
	int ret;
	struct lio_node* node;
	char name[16] = {0};

	lio_dev->class = class_create(THIS_MODULE, CLASS_NAME);
	if (lio_dev->class == NULL)
	{
		printk("[%s] class create fail\n", __func__);
		goto FAIL;
	}

	list_for_each_entry(node, &lio_dev->lio_nodes, list) {
		cdev_init(&node->cdev, &fops);
		ret = cdev_add(&node->cdev, node->dev_num, 1);
		if (ret != 0)
		{
			printk("[%s] cdev_add fail <%d = %d:%d>\n", __func__,
					ret, MAJOR(node->dev_num), MINOR(node->dev_num));
			goto FAIL;
		}

		snprintf(name, sizeof(name), "%s%d",
				DEVICE_NAME_FREFIX, node->index);
		node->dev = device_create(lio_dev->class,
				NULL, node->dev_num, NULL, name);
		if (node->dev == NULL)
		{
			printk("[%s] device_create fail <%d = %s %d:%d>\n", __func__,
					ret, name,
					MAJOR(node->dev_num), MINOR(node->dev_num));
			goto FAIL;
		}
		printk("device create success.%s %d:%d\n",
				name, MAJOR(node->dev_num), MINOR(node->dev_num));
	}

	return 0;

FAIL:
	lio_remove_device_node(lio_dev);
	return -1;
}

static int lio_probe(struct platform_device *pdev)
{
	int ret;
	struct lio_dev_s* lio_dev = &g_lio_dev;

	lio_record_config(pdev->dev.of_node, lio_dev);
	lio_sync_config_to_reg(pdev->dev.of_node, lio_dev);

	ret = lio_aquire_resource(pdev, lio_dev);
	if (ret != 0)
		goto FAIL;

	ret = lio_alloc_device_region(lio_dev);
	if (ret != 0)
		goto RELEASE_RES;

	ret = lio_create_device_node(lio_dev);
	if (ret != 0)
		goto FREE_DEVREG;

	return 0;

FREE_DEVREG:
	printk("[%s] create device node fail\n", __func__);
	lio_free_device_region(lio_dev);
RELEASE_RES:
	printk("[%s] alloc device region fail\n", __func__);
	lio_release_resource(lio_dev);
FAIL:
	printk("[%s] acquire fail\n", __func__);
	return -EFAULT;
}

int lio_remove(struct platform_device *pdev)
{
	struct lio_dev_s* lio_dev = &g_lio_dev;

	lio_remove_device_node(lio_dev);
	lio_free_device_region(lio_dev);
	lio_release_resource(lio_dev);
	return 0;
}

const struct of_device_id lio_id_table[] = {
	{.compatible = "loongson,lio"},
	{},
};

struct platform_driver lio_driver = {
	.probe = lio_probe,
	.remove = lio_remove,
	.driver = {
		.name = "ls2k-lio",
		.of_match_table = of_match_ptr(lio_id_table)
	},
};

static int __init lio_init(void)
{
	return platform_driver_register(&lio_driver);
}

static void __exit lio_exit(void)
{
	platform_driver_unregister(&lio_driver);
}

module_init(lio_init);
module_exit(lio_exit);

MODULE_LICENSE("GPL");
